import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
import os

# =================================================================================
# 配置区域：定义需要忽略的文件夹、文件扩展名和特定文件名
# 您可以根据自己的需求修改这些列表
# =================================================================================

# 1. 需要完全忽略的文件夹名称列表
# 无论在哪个路径下，只要文件夹名匹配，就会被跳过
IGNORE_DIRS = {
    '.git', '.svn', '.hg', '.idea', '.vscode', 'node_modules',
    '__pycache__', 'build', 'dist', 'venv', 'target',
    'cmake-build-debug', 'cmake-build-release'
}

# 2. 需要忽略的文件扩展名列表
# 匹配这些扩展名的文件将被跳过
IGNORE_EXTENSIONS = {
    # 编译和打包文件
    '.pyc', '.pyo', '.pyd', '.o', '.so', '.a', '.lib', '.dll', '.exe',
    '.class', '.jar', '.war', '.ear',
    # 日志和临时文件
    '.log', '.tmp', '.swp', '.swo', '.bak',
    # 数据库文件
    '.db', '.sqlite', '.sqlite3',
    # 压缩文件
    '.zip', '.tar', '.gz', '.rar', '.7z',
    # 媒体和文档文件
    '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.ico', '.svg',
    '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
    '.mp3', '.wav', '.mp4', '.avi', '.mov', '.mkv', '.webm',
    # 字体文件
    '.woff', '.woff2', '.ttf', '.eot',
}

# 3. 需要忽略的特定文件名列表
IGNORE_FILES = {
    '.DS_Store', 'Thumbs.db', 'desktop.ini'
}


class CodeAggregatorApp:
    """
    代码聚合器主应用程序类
    """
    def __init__(self, root_window):
        self.root = root_window
        self.root.title("代码聚合与分析工具")
        self.root.geometry("1000x750")

        # --- UI 布局 ---
        # 顶部框架
        top_frame = tk.Frame(self.root, padx=10, pady=10)
        top_frame.pack(fill=tk.X)

        # 文件夹选择按钮
        self.select_button = tk.Button(top_frame, text="选择项目文件夹", command=self.select_folder, width=15)
        self.select_button.pack(side=tk.LEFT)

        # 一键复制按钮
        self.copy_button = tk.Button(top_frame, text="一键复制全部", command=self.copy_to_clipboard, width=15)
        self.copy_button.pack(side=tk.LEFT, padx=(10, 0))

        # Token 统计标签
        self.token_label = tk.Label(top_frame, text="总Token估算: 0", font=("Arial", 10))
        self.token_label.pack(side=tk.RIGHT)

        # 状态标签
        self.status_label = tk.Label(top_frame, text="请先选择一个文件夹", fg="blue")
        self.status_label.pack(side=tk.LEFT, padx=(20, 0))

        # 中部主文本显示区域
        self.text_area = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, undo=True, font=("Courier New", 10))
        self.text_area.pack(expand=True, fill=tk.BOTH, padx=10, pady=(0, 10))

    def select_folder(self):
        """
        打开对话框让用户选择文件夹，并触发处理流程
        """
        folder_path = filedialog.askdirectory(title="请选择一个项目文件夹")
        if folder_path:
            self.status_label.config(text=f"正在处理: {folder_path}", fg="orange")
            self.root.update()  # 立即更新UI显示状态
            try:
                self.process_folder(folder_path)
                self.status_label.config(text=f"处理完成: {os.path.basename(folder_path)}", fg="green")
            except Exception as e:
                self.status_label.config(text=f"发生错误: {e}", fg="red")
                messagebox.showerror("处理错误", f"在处理文件夹时发生错误:\n{e}")

    def process_folder(self, folder_path):
        """
        核心处理函数：生成结构树、读取文件内容、计算Token
        """
        # 1. 清空旧内容
        self.text_area.delete('1.0', tk.END)
        self.token_label.config(text="总Token估算: 0")

        # 2. 生成并显示项目结构树
        tree_header = f"项目 '{os.path.basename(folder_path)}' 的结构树:\n"
        tree_structure = self.generate_tree(folder_path)
        self.text_area.insert(tk.END, tree_header + tree_structure + "\n" + "="*80 + "\n\n")

        # 3. 遍历文件，读取并显示内容
        all_content_for_token_calc = [tree_structure]
        for root, dirs, files in os.walk(folder_path, topdown=True):
            # 过滤需要忽略的目录
            dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]

            for filename in sorted(files):
                # 过滤需要忽略的文件
                if filename in IGNORE_FILES or os.path.splitext(filename)[1] in IGNORE_EXTENSIONS:
                    continue

                file_path = os.path.join(root, filename)
                relative_path = os.path.relpath(file_path, folder_path)

                try:
                    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                        
                        # 准备要显示和用于计算的内容
                        file_header = f"--- 文件路径: {relative_path} ---\n\n"
                        file_full_content = file_header + content + "\n\n"
                        
                        # 插入到显示框
                        self.text_area.insert(tk.END, file_full_content)
                        
                        # 添加到用于Token计算的列表
                        all_content_for_token_calc.append(file_full_content)

                except Exception as e:
                    error_message = f"--- 无法读取文件: {relative_path} (错误: {e}) ---\n\n"
                    self.text_area.insert(tk.END, error_message)
        
        # 4. 计算并更新总Token估算值
        final_text = "".join(all_content_for_token_calc)
        token_count = self.estimate_tokens(final_text)
        self.token_label.config(text=f"总Token估算: {self.format_tokens(token_count)}")

    def generate_tree(self, start_path):
        """
        生成目录结构树字符串
        """
        tree_lines = []
        for root, dirs, files in os.walk(start_path, topdown=True):
            # 过滤需要忽略的目录
            dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
            
            level = root.replace(start_path, '').count(os.sep)
            indent = ' ' * 4 * level
            
            # 添加目录
            tree_lines.append(f"{indent}📂 {os.path.basename(root)}/")
            
            sub_indent = ' ' * 4 * (level + 1)
            # 添加文件
            for f in sorted(files):
                if f in IGNORE_FILES or os.path.splitext(f)[1] in IGNORE_EXTENSIONS:
                    continue
                tree_lines.append(f"{sub_indent}📄 {f}")
        
        # 移除根目录自身的重复显示，使结构更清晰
        if tree_lines:
            tree_lines[0] = f"📂 {os.path.basename(start_path)}/"
        return "\n".join(tree_lines)

    def estimate_tokens(self, text):
        """
        估算文本的Token数量。
        这是一个简化的估算方法，基于字符数。
        对于英文和代码，通常一个Token约等于4个字符。这里我们使用一个近似值。
        """
        return len(text)

    def format_tokens(self, count):
        """
        将Token数量格式化为K或M单位
        """
        if count >= 1_000_000:
            return f"{count / 1_000_000:.2f} M"
        elif count >= 1_000:
            return f"{count / 1_000:.2f} K"
        else:
            return str(count)

    def copy_to_clipboard(self):
        """
        将文本区域的全部内容复制到系统剪贴板
        """
        content = self.text_area.get('1.0', tk.END)
        if not content.strip():
            messagebox.showinfo("提示", "没有内容可以复制。")
            return
            
        self.root.clipboard_clear()
        self.root.clipboard_append(content)
        self.root.update()  # 在某些系统上需要此操作来确保剪贴板更新
        messagebox.showinfo("成功", "所有内容已成功复制到剪贴板！")


if __name__ == "__main__":
    main_window = tk.Tk()
    app = CodeAggregatorApp(main_window)
    main_window.mainloop()
